Skip to content

Conversation

nielsdos
Copy link
Member

Due to closures, the fbc address isn't unique if the memory address is reused. So for user closures we need to distinguish using a unique key, while internal functions can't disappear and therefore can use the fbc address as unique key.

As for performance:
This adds a load+compare for the function type, although we already load and compare the function type down below, so that impact on performance should be small. It also adds a load on the opcodes array, but we access fields from the same cacheline too so that should also have only a small impact.
It can make a performance improvement for shared opcodes arrays in case of traits as the cache slot can be reused.

…losures can cause a crash

Due to closures, the `fbc` address isn't unique if the memory address is reused.
So for user closures we need to distinguish using a unique key,
while internal functions can't disappear and therefore can use the `fbc` address
as unique key.

As for performance:
This adds a load+compare for the function type, although we already load
and compare the function type down below, so that impact on performance
should be small. It also adds a load on the opcodes array, but we access
fields from the same cacheline too so that should also have only a small
impact.
@nielsdos nielsdos marked this pull request as ready for review August 31, 2025 20:10
@nielsdos nielsdos requested a review from dstogov as a code owner August 31, 2025 20:10
Copy link
Member

@dstogov dstogov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, I didn't understand the problem from the explanation at the first place, but when I started writing a comment, I found that the explanation says almost the same that I liked to say :) - "fbc" can't be stored in cache, because PHP may free the closure and allocate another one (with the same address).

Comment on lines +5061 to +5064
/* Due to closures, the `fbc` address isn't unique if the memory address is reused.
* So for user closures we need to distinguish using a unique key, while internal functions can't disappear
* and therefore can use the `fbc` address as unique key. */
void *unique_id = EXPECTED(fbc->type == ZEND_USER_FUNCTION) ? (void *) fbc->op_array.opcodes : (void *) fbc;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fbc->op_array.opcodes is not unique unless ZEND_ACC_IMMUTABLE is set. E.g.

function usage1(Closure $c) {
    $c(a: 1);
}

usage1(eval('return function($a) { var_dump($a); };'));
usage1(eval('return function($b) { var_dump($b); };'));

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good (but unfortunate) find.
This should be a rare though. What about avoiding the use of a cache slot in the case ZEND_ACC_IMMUTABLE is unset?

Copy link
Member

@arnaud-lb arnaud-lb Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this sounds good

Copy link
Member

@bwoebi bwoebi Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also only is problematic for files and file-bound op_arrays. I.e. for anything named that's perfectly fine.

As a suggestion to avoid branching on fbc->type, you might use fbc->common.arg_info as key instead.

Avoiding the use of a cache slot when ZEND_ACC_IMMUTABLE is unset essentially disables the optimization for every closure call though, which is unfortunate (as they aren't immutable at all, only the prototype is).

Copy link
Member

@bwoebi bwoebi Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally there would be a fn_flag which would indicate whether something is persistent or not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fbc->common.arg_info can be shared among internal functions, no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose then it doesn't matter because functions always ahve the same argument ordering then

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoiding the use of a cache slot when ZEND_ACC_IMMUTABLE is unset essentially disables the optimization for every closure call though, which is unfortunate (as they aren't immutable at all, only the prototype is).

Yeah okay this is no good.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can check that fbc->op_array.refcount is NULL as a proxy for "op array is persistent", but that requires branching.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Closure named argument unpacking between temporary closures can cause a crash
4 participants